"""
设备管理业务逻辑层
- 所有业务校验、流程编排、事务控制均在此层完成
- 只返回业务结果或抛 BizException，不做响应封装
- 自动记录设备操作日志(CREATE/UPDATE/DELETE)
"""

import json
from datetime import datetime
from typing import Optional

from app.common.code_generator import get_device_code_generator, CodeGenerationError
from app.crud.device_crud import (
	get_station_by_id,
	get_station_by_code,
	create_station,
	get_box_by_id,
	get_box_by_code,
	create_box,
	get_rtu_by_serial_no,
	create_rtu,
	get_meter_by_id,
	get_meter_by_code,
	get_meter_by_serial_no,
	create_meter,
	get_box_list,
	get_rtu_list,
	get_meter_list,
	update_meter,
	get_station_list,
	get_meter_with_address,
	get_rtu_with_address, get_rtu_by_id, update_box, update_rtu,
)
from app.crud.file_crud import get_files_by_business
from app.schemas.device_schemas import (
	StationCreate,
	BoxCreate,
	RTUCreate,
	MeterCreate,
	MeterUpdate,
	BoxUpdate,
	RTUUpdate,
	DeviceQuery,
	PaginatedStation,
	StationResponse,
	PaginatedBox,
	BoxResponse,
	PaginatedRTU,
	RTUResponse,
	DeviceTreeNode,
	DeviceLocationInfo, MeterResponse,
)
from app.service.file_service import file_service
from common.base_enums import DeviceStatus, WorkOrderDeviceType, DeviceType
from common.database import DatabaseService
from common.exceptions import BizException, ErrorCode
from common.logger import create_logger

logger = create_logger("device_service")

# ---------- 常量 ----------
STATION_CODE_LEN = 14
BOX_CODE_LEN = 19
METER_CODE_LEN = 24

STATION_PREFIX = "T"
BOX_PREFIX = "B"
METER_PREFIX = "M"

VALID_PHASES = {"A", "B", "C", "N"}


# ---------- 私有工具 ----------
def _check_date_range(start: Optional[datetime], end: Optional[datetime]):
	if start and end and start >= end:
		raise BizException.from_error_code(
			ErrorCode.BAD_REQUEST, message="开始日期必须早于结束日期"
		)


async def _log_device_operation(
		db: DatabaseService,
		device_type: str,
		device_id: int,
		op_type: str,
		op_by: int,
		note: Optional[str] = None,
		old_vals: Optional[dict] = None,
		new_vals: Optional[dict] = None
) -> None:
	"""
	记录设备操作日志

	Args:
		db: 数据库服务
		device_type: 设备类型 (STATION/BOX/RTU/METER)
		device_id: 设备ID
		op_type: 操作类型 (CREATE/UPDATE/DELETE/STATUS_CHANGE)
		op_by: 操作人 ID
		note: 备注
		old_vals: 变更前值 (dict)
		new_vals: 变更后值 (dict)
	"""
	try:
		from app.crud.device_event_crud import create_device_operation_log

		# 将dict转换为JSON字符串
		old_vals_json = json.dumps(old_vals, ensure_ascii=False, default=str) if old_vals else None
		new_vals_json = json.dumps(new_vals, ensure_ascii=False, default=str) if new_vals else None

		await create_device_operation_log(db, {
			"target_type": device_type,
			"target_id": device_id,
			"op_type": op_type,
			"op_by": op_by,
			"op_at": datetime.now(),
			"note": note,
			"old_vals": old_vals_json,
			"new_vals": new_vals_json
		})
		logger.info(f"记录设备操作日志: {device_type}/{device_id}, 操作={op_type}")
	except Exception as e:
		# 日志记录失败不影响业务流程,只记录警告
		logger.warning(f"记录设备操作日志失败: {device_type}/{device_id}, error={e}")


# ============ 台区 ============
class DeviceService:
	"""设备管理服务类"""

	@staticmethod
	async def validate_station_code(code: str) -> bool:
		if (
				not code
				or len(code) != STATION_CODE_LEN
				or not code.startswith(STATION_PREFIX)
		):
			return False
		province_city_area = code[1:10]
		serial = code[10:]
		return province_city_area.isdigit() and serial.isdigit()

	@staticmethod
	async def create_station_with_validation(
			db: DatabaseService, payload: StationCreate, operator_id: int = None
	) -> int:

		# 自动生成编码
		try:
			code_generator = get_device_code_generator()
			generated_code = await code_generator.generate_station_code(db, payload.address_id)
			payload.code = generated_code
			logger.info(f"自动生成台区编码: {generated_code}")
		except CodeGenerationError as e:
			raise BizException.from_error_code(
				ErrorCode.INTERNAL_SERVER_ERROR, message=f"台区编码生成失败: {e}"
			) from e

		if await get_station_by_code(db, payload.code):
			raise BizException.from_error_code(
				ErrorCode.CONFLICT, message=f"台区编码 {payload.code} 已存在"
			)

		try:
			station_id = await create_station(db, payload)

			# 记录操作日志
			if operator_id:
				await _log_device_operation(
					db=db,
					device_type="STATION",
					device_id=station_id,
					op_type="CREATE",
					op_by=operator_id,
					note=f"创建台区: {payload.name}",
					new_vals=payload.model_dump()
				)

			return station_id
		except Exception as e:
			logger.exception("创建台区失败，payload=%s", payload)
			raise BizException.from_error_code(
				ErrorCode.INTERNAL_SERVER_ERROR, message="创建台区失败"
			) from e

	# ============ 电箱 ============
	@staticmethod
	async def validate_box_code(code: str) -> bool:
		if not code or len(code) != BOX_CODE_LEN or not code.startswith(BOX_PREFIX):
			return False
		station_code = code[1:15]
		serial = code[15:]
		if len(serial) != 4 or not serial.isdigit():
			return False
		return await DeviceService.validate_station_code(station_code)

	@staticmethod
	async def create_box_with_validation(
			db: DatabaseService, payload: BoxCreate, operator_id: Optional[int] = None
	) -> int:
		# 验证台区存在
		station = await get_station_by_id(db, payload.station_id)
		if not station:
			raise BizException.from_error_code(
				ErrorCode.NOT_FOUND, message="所属台区不存在"
			)

		try:
			code_generator = get_device_code_generator()

			generated_code = await code_generator.generate_box_code(station.code, db)
			payload.code = generated_code
			logger.info(f"自动生成电箱编码: {generated_code}")
		except CodeGenerationError as e:
			raise BizException.from_error_code(
				ErrorCode.INTERNAL_SERVER_ERROR, message=f"电箱编码生成失败: {e}"
			) from e

		if await get_box_by_code(db, payload.code):
			raise BizException.from_error_code(
				ErrorCode.CONFLICT, message=f"电箱编码 {payload.code} 已存在"
			)

		# 开始事务处理
		async with db.transaction():  # 使用数据库事务
			try:
				# 1. 创建电箱
				box_id = await create_box(db, payload)
				logger.info(f"创建电箱成功，box_id={box_id}")

				# 2. 如果有文件ID，进行文件绑定
				if payload.file_ids and len(payload.file_ids) > 0:
					logger.info(
						f"开始绑定文件到电箱，box_id={box_id}, file_ids={payload.file_ids}"
					)
					bind_result = await file_service.batch_bind_files_to_business(
						db, payload.file_ids, WorkOrderDeviceType.BOX.value, str(box_id)
					)
					logger.info(f"文件绑定结果：{bind_result}")

				# 3. 记录操作日志
				if operator_id:
					await _log_device_operation(
						db=db,
						device_type="BOX",
						device_id=box_id,
						op_type="CREATE",
						op_by=operator_id,
						note=f"创建电箱,所属台区名称:{station.name}",
						new_vals=payload.model_dump()
					)

				return box_id
			except Exception as e:
				logger.exception("创建电箱失败，payload=%s", payload)
				raise BizException.from_error_code(
					ErrorCode.INTERNAL_SERVER_ERROR, message="创建电箱失败"
				) from e

	# ============ RTU ============
	@staticmethod
	async def create_rtu_with_validation(
			db: DatabaseService, payload: RTUCreate, operator_id: Optional[int] = None
	) -> int:
		# 验证电箱存在
		box = await get_box_by_id(db, payload.box_id)
		if not box:
			raise BizException.from_error_code(
				ErrorCode.NOT_FOUND, message="所属电箱不存在"
			)

		try:
			code_generator = get_device_code_generator()

			# 生成编码,如果与已删除的RTU冲突,则重新生成
			max_retries = 3
			for attempt in range(max_retries):
				generated_code = await code_generator.generate_rtu_code(box.code, "box", db)
				
				# 检查编码是否与已存在的RTU冲突(包括已删除的)
				check_sql = "SELECT id FROM dev_rtu WHERE code = %s LIMIT 1"
				existing = db.get_one(check_sql, (generated_code,))
				
				if not existing:
					payload.code = generated_code
					logger.info(f"自动生成RTU编码: {generated_code}")
					break
				else:
					logger.warning(f"编码 {generated_code} 与已存在的RTU冲突(可能已删除),尝试重新生成({attempt+1}/{max_retries})")
					if attempt == max_retries - 1:
						raise BizException.from_error_code(
							ErrorCode.INTERNAL_SERVER_ERROR, 
							message=f"RTU编码生成失败: 多次尝试后仍与已存在的编码冲突"
						)
		except CodeGenerationError as e:
			raise BizException.from_error_code(
				ErrorCode.INTERNAL_SERVER_ERROR, message=f"RTU编码生成失败: {e}"
			) from e

		# 检查序列号唯一性
		if payload.serial_no and await get_rtu_by_serial_no(db, payload.serial_no):
			raise BizException.from_error_code(
				ErrorCode.CONFLICT, message=f"RTU序列号 {payload.serial_no} 已存在"
			)

		# 检查是否已在事务中(检查autocommit状态)
		in_transaction = not getattr(db.conn, 'autocommit', True)

		if in_transaction:
			# 已在事务中,直接执行不开启新事务
			try:
				# 1. 创建RTU
				rtu_id = await create_rtu(db, payload)
				logger.info(f"创建RTU成功，rtu_id={rtu_id}")

				# 2. 如果有文件ID，进行文件绑定
				if getattr(payload, "file_ids", None) and len(payload.file_ids) > 0:
					logger.info(
						f"开始绑定文件到RTU，rtu_id={rtu_id}, file_ids={payload.file_ids}"
					)
					bind_result = await file_service.batch_bind_files_to_business(
						db, payload.file_ids, "RTU", str(rtu_id)
					)
					logger.info(f"文件绑定结果：{bind_result}")

				# 3. 记录操作日志
				if operator_id:
					await _log_device_operation(
						db=db,
						device_type="RTU",
						device_id=rtu_id,
						op_type="CREATE",
						op_by=operator_id,
						note=f"创建RTU,所属电箱序列号:{box.serial_no or 'N/A'}",
						new_vals=payload.model_dump()
					)

				return rtu_id
			except Exception as e:
				logger.exception("创建RTU失败，payload=%s", payload)
				raise BizException.from_error_code(ErrorCode.INTERNAL_SERVER_ERROR, message="创建RTU失败") from e
		else:
			# 未在事务中,开启新事务
			async with db.transaction():
				try:
					# 1. 创建RTU
					rtu_id = await create_rtu(db, payload)
					logger.info(f"创建RTU成功，rtu_id={rtu_id}")

					# 2. 如果有文件ID，进行文件绑定
					if getattr(payload, "file_ids", None) and len(payload.file_ids) > 0:
						logger.info(
							f"开始绑定文件到RTU，rtu_id={rtu_id}, file_ids={payload.file_ids}"
						)
						bind_result = await file_service.batch_bind_files_to_business(
							db, payload.file_ids, "RTU", str(rtu_id)
						)
						logger.info(f"文件绑定结果：{bind_result}")

					# 3. 记录操作日志
					if operator_id:
						await _log_device_operation(
							db=db,
							device_type="RTU",
							device_id=rtu_id,
							op_type="CREATE",
							op_by=operator_id,
							note=f"创建RTU,所属电箱序列号:{box.serial_no or 'N/A'}",
							new_vals=payload.model_dump()
						)

					return rtu_id
				except Exception as e:
					logger.exception("创建RTU失败，payload=%s", payload)
					raise BizException.from_error_code(ErrorCode.INTERNAL_SERVER_ERROR, message="创建RTU失败") from e

	# ============ 电表 ============
	@staticmethod
	async def validate_meter_code(code: str) -> bool:
		"""
		电表编码规则：
		1. 必须以 M 开头
		2. 后 4 位 = 相别(ABCN) + 3 位数字流水
		3. 中间段 = RTU 编码，仅校验字符合法（长度不限）
		4. 总长度 >= 6（1+M+后4）
		"""
		import re

		# 后 4 位：1 位相别 + 3 位流水
		METER_TAIL_RE = re.compile(r"^[ABCN]\d{3}$")

		# 中间段：仅允许字母+数字+下划线，避免特殊符号
		RTU_CODE_RE = re.compile(r"^[A-Z0-9_]+$", flags=re.IGNORECASE)

		if not code or len(code) < 6:
			return False
		if not code.startswith("M"):
			return False

		tail = code[-4:]
		rtu_code = code[1:-4]

		return (
				METER_TAIL_RE.match(tail) is not None
				and RTU_CODE_RE.match(rtu_code) is not None
		)

	@staticmethod
	async def create_meter_with_validation(
			db: DatabaseService, payload: MeterCreate, operator_id: Optional[int] = None
	) -> int:

		# 显式触发 Pydantic 所有校验
		payload = MeterCreate.model_validate(payload.model_dump())

		# 验证RTU存在
		rtu = None
		if payload.rtu_id:
			rtu = await get_rtu_by_id(db, payload.rtu_id)
			if not rtu:
				raise BizException.from_error_code(
					ErrorCode.NOT_FOUND, message="所属RTU不存在"
				)
			# 自动获取box_id
			if not payload.box_id:
				payload.box_id = rtu.box_id

			# 验证collector_type=Master时的规则
			if rtu.collector_type == 'Master' and payload.meter_type and payload.meter_type.value == 'SINGLE':
				raise BizException.from_error_code(
					ErrorCode.BAD_REQUEST,
					message=f"Master类型RTU的必须需要绑定三相表"
				)

			if payload.box_id != rtu.box_id:
				raise BizException.from_error_code(
					ErrorCode.BAD_REQUEST,
					message=f"Master类型RTU的必须与单相表在同一电箱"
				)

		# 验证单相表的parent_meter_id
		if payload.meter_type and payload.meter_type.value == 'SINGLE' and payload.parent_meter_id:
			# 验证三相表存在且类型为THREE_PHASE
			parent_meter = await get_meter_by_id(db, payload.parent_meter_id)
			if not parent_meter:
				raise BizException.from_error_code(
					ErrorCode.NOT_FOUND, message=f"上级三相表 {payload.parent_meter_id} 不存在"
				)
			if parent_meter.meter_type != 'THREE_PHASE':
				raise BizException.from_error_code(
					ErrorCode.BAD_REQUEST, message="parent_meter_id必须指向三相表"
				)
			if not payload.phase:
				raise BizException.from_error_code(
					ErrorCode.BAD_REQUEST, message="单相表必须指定相别"
				)

		try:
			code_generator = get_device_code_generator()
			# 传入数据库实例以支持数据库降级功能
			generated_code = await code_generator.generate_meter_code(rtu.code, db)
			payload.code = generated_code
			logger.info(f"自动生成电表编码: {generated_code}")
		except CodeGenerationError as e:
			raise BizException.from_error_code(
				ErrorCode.INTERNAL_SERVER_ERROR, message=f"电表编码生成失败: {e}"
			) from e

		# 检查编码和序列号唯一性
		if await get_meter_by_code(db, payload.code):
			raise BizException.from_error_code(
				ErrorCode.CONFLICT, message=f"电表编码 {payload.code} 已存在"
			)
		if payload.serial_no and await get_meter_by_serial_no(db, payload.serial_no):
			raise BizException.from_error_code(
				ErrorCode.CONFLICT, message=f"电表序列号 {payload.serial_no} 已存在"
			)
		if payload.ct_ratio <= 0 or payload.pt_ratio <= 0:
			raise BizException.from_error_code(
				ErrorCode.BAD_REQUEST, message="CT/PT变比必须大于0"
			)

		# 创建 - 开始事务处理
		async with db.transaction():
			try:
				# 创建电表
				meter_id = await create_meter(db, payload)
				logger.info(f"创建电表成功，meter_id={meter_id}")

				# 如果有文件ID，进行文件绑定
				if getattr(payload, "file_ids", None) and len(payload.file_ids) > 0:
					logger.info(
						f"开始绑定文件到电表，meter_id={meter_id}, file_ids={payload.file_ids}"
					)
					bind_result = await file_service.batch_bind_files_to_business(
						db, payload.file_ids, "METER", str(meter_id)
					)
					logger.info(f"文件绑定结果：{bind_result}")

				# 记录操作日志
				if operator_id:
					await _log_device_operation(
						db=db,
						device_type="METER",
						device_id=meter_id,
						op_type="CREATE",
						op_by=operator_id,
						note=f"创建电表,所属RTU序列号:{rtu.serial_no or 'N/A'}",
						new_vals=payload.model_dump()
					)

				return meter_id
			except Exception as e:
				logger.exception("创建电表失败，payload=%s", payload)
				raise BizException.from_error_code(
					ErrorCode.INTERNAL_SERVER_ERROR, message="创建电表失败"
				) from e

	@staticmethod
	async def _update_device_files(db: DatabaseService, device_id: int, biz_type: str,
								   file_ids: list = None, add_file_ids: list = None,
								   del_file_ids: list = None) -> dict:
		"""
		通用设备文件更新方法

		Args:
			db: 数据库服务
			device_id: 设备ID
			biz_type: 业务类型
			file_ids: 新的文件ID列表（全量模式），如果提供则忽略add_file_ids和del_file_ids
			add_file_ids: 要添加的文件ID列表（增量模式）
			del_file_ids: 要删除的文件ID列表（增量模式）

		Returns:
			dict: 文件更新结果
		"""
		return await file_service.update_files_for_business(
			db=db,
			biz_type=biz_type,
			biz_id=str(device_id),
			new_file_ids=file_ids,
			add_file_ids=add_file_ids,
			del_file_ids=del_file_ids
		)

	@staticmethod
	async def _update_device_with_file(db: DatabaseService, device_id: int, payload, update_func,
									   device_type: str, device_type_value: str, error_message: str) -> bool:
		"""
		通用设备更新方法，处理设备基本信息更新和文件更新

		Args:
			db: 数据库服务
			device_id: 设备ID
			payload: 设备更新对象
			update_func: 设备更新函数
			device_type: 设备类型（用于日志）
			device_type_value: 设备类型值（用于文件更新）
			error_message: 错误信息

		Returns:
			bool: 更新是否成功
		"""
		try:
			async with db.transaction():
				# 更新设备基本信息
				success = await update_func(db, device_id, payload)
				if not success:
					return False

				# 处理文件更新
				has_file_updates = (hasattr(payload, 'file_ids') and payload.file_ids is not None) or \
								   (hasattr(payload, 'add_file_ids') and payload.add_file_ids) or \
								   (hasattr(payload, 'del_file_ids') and payload.del_file_ids)

				if has_file_updates:
					file_ids = getattr(payload, 'file_ids', None)
					add_file_ids = getattr(payload, 'add_file_ids', None)
					del_file_ids = getattr(payload, 'del_file_ids', None)
					await DeviceService._update_device_files(db, device_id, device_type_value, file_ids,
															 add_file_ids, del_file_ids)

				return True
		except BizException:
			# BizException（包括设备不存在、关系验证失败等）直接向上抛出，保留原有错误信息
			raise
		except Exception as e:
			# 其他异常（数据库错误等）包装为DATABASE_ERROR
			logger.exception(f"更新{device_type}失败，{device_type}_id={device_id}, payload={payload}")
			raise BizException.from_error_code(ErrorCode.DATABASE_ERROR, message=error_message) from e

	@staticmethod
	async def update_box_with_file_update(db: DatabaseService, box_id: int,
										  payload: BoxUpdate) -> bool:
		"""
		更新电箱信息并处理文件更新
		注意: 操作日志由Router层记录,确保只有完全成功后才记录
		"""
		return await DeviceService._update_device_with_file(
			db, box_id, payload, update_box,
			'电箱', WorkOrderDeviceType.BOX.value, "更新电箱失败"
		)

	@staticmethod
	async def update_rtu_with_file_update(db: DatabaseService, rtu_id: int,
										  payload: RTUUpdate) -> bool:
		"""
		更新RTU信息并处理文件更新
		注意: 操作日志由Router层记录,确保只有完全成功后才记录
		"""
		return await DeviceService._update_device_with_file(
			db, rtu_id, payload, update_rtu,
			'RTU', WorkOrderDeviceType.RTU.value, "更新RTU失败"
		)

	@staticmethod
	async def update_meter_with_file_update(db: DatabaseService, meter_id: int,
											payload: MeterUpdate) -> bool:
		"""
		更新电表信息并处理文件更新
		注意: 操作日志由Router层记录,确保只有完全成功后才记录
		"""
		# 验证rtu_id和box_id的关系
		if payload.rtu_id is not None or payload.box_id is not None:
			# 获取当前电表信息
			current_meter = await get_meter_by_id(db, meter_id)
			if not current_meter:
				raise BizException.from_error_code(
					ErrorCode.NOT_FOUND, message="电表不存在"
				)

			# 确定rtu_id和box_id（使用新值或旧值）
			final_rtu_id = payload.rtu_id if payload.rtu_id is not None else current_meter.rtu_id
			final_box_id = payload.box_id if payload.box_id is not None else current_meter.box_id

			# 如果有rtu_id，验证RTU存在且collector_type规则
			if final_rtu_id:
				rtu = await get_rtu_by_id(db, final_rtu_id)
				if not rtu:
					raise BizException.from_error_code(
						ErrorCode.NOT_FOUND, message="RTU不存在"
					)

				# 判断是否为单相表：使用payload中的meter_type或current_meter的meter_type
				final_meter_type = payload.meter_type if payload.meter_type is not None else current_meter.meter_type
				if rtu.collector_type == 'Master' and final_meter_type == 'SINGLE':
					raise BizException.from_error_code(
						ErrorCode.BAD_REQUEST,
						message=f"Master类型RTU的必须与三相表绑定"
					)

				if final_box_id != rtu.box_id:
					raise BizException.from_error_code(
						ErrorCode.BAD_REQUEST,
						message=f"电表RTU与新的电箱信息不一致"
					)

		return await DeviceService._update_device_with_file(
			db, meter_id, payload, update_meter,
			'电表', 'meter', "更新电表失败"
		)

	# ============ 设备树 ============
	@staticmethod
	async def get_device_tree(db: DatabaseService, station_id: int) -> DeviceTreeNode:
		station = await get_station_by_id(db, station_id)
		if not station:
			raise BizException.from_error_code(
				ErrorCode.NOT_FOUND, message="台区不存在"
			)

		_, boxes = await get_box_list(db, DeviceQuery(page=1, size=1000))
		station_boxes = [b for b in boxes if b.station_id == station_id]

		children = []
		for box in station_boxes:
			_, rtus = await get_rtu_list(db, DeviceQuery(page=1, size=1000))
			box_rtus = [r for r in rtus if r.box_id == box.id]

			rtu_children = []
			for rtu in box_rtus:
				_, meters = await get_meter_list(db, DeviceQuery(page=1, size=1000))
				rtu_meters = [m for m in meters if m.rtu_id == rtu.id]

				meter_children = [
					DeviceTreeNode(
						id=meter.id,
						code=meter.code,
						serial_no=meter.serial_no,
						name=None,
						phase=meter.phase,
						device_type="METER",
						status=meter.status,
						children=[],
					)
					for meter in rtu_meters
				]

				rtu_children.append(
					DeviceTreeNode(
						id=rtu.id,
						code=rtu.code,
						serial_no=rtu.serial_no,
						name=None,
						device_type="RTU",
						status=rtu.status,
						children=meter_children,
					)
				)

			children.append(
				DeviceTreeNode(
					id=box.id,
					code=box.code,
					serial_no=box.serial_no,
					name=None,
					device_type="BOX",
					status=box.status,
					children=rtu_children,
				)
			)

		return DeviceTreeNode(
			id=station.id,
			code=station.code,
			serial_no=station.serial_no,
			name=station.name,
			device_type="STATION",
			status=station.status,
			children=children,
		)

	# ============ 获取RTU-电表树 ============
	@staticmethod
	async def get_rtu_meter_tree(db: DatabaseService, box_id: int) -> DeviceTreeNode:
		"""
		根据电箱ID获取RTU-电表完整树
		"""
		box = await get_box_by_id(db, box_id)
		if not box:
			raise BizException.from_error_code(
				ErrorCode.NOT_FOUND, message="电箱不存在"
			)

		_, rtus = await get_rtu_list(db, DeviceQuery(page=1, size=100, box_id=box_id))
		box_rtus = [r for r in rtus if r.box_id == box_id]

		children = []
		for rtu in box_rtus:
			_, meters = await get_meter_list(db, DeviceQuery(page=1, size=100, rtu_id=rtu.id))
			rtu_meters = [m for m in meters if m.rtu_id == rtu.id]

			# 获取电表及其地址信息
			meter_children = []
			for meter in rtu_meters:
				meter_with_address = await get_meter_with_address(db, meter.id)
				location_info = []
				if meter_with_address and "location_info" in meter_with_address:
					location_info = [
						DeviceLocationInfo(**loc)
						for loc in meter_with_address["location_info"]
					]
				meter_children.append(
					DeviceTreeNode(
						id=meter.id,
						code=meter.code,
						serial_no=meter.serial_no or "",
						name=None,
						device_type=DeviceType.METER.value,
						status=meter.status,
						location_info=location_info,
						children=[],
					)
				)

			# 获取RTU及其地址信息
			rtu_with_address = await get_rtu_with_address(db, rtu.id)
			location_info = []
			if rtu_with_address and "location_info" in rtu_with_address:
				location_info = [
					DeviceLocationInfo(**loc)
					for loc in rtu_with_address["location_info"]
				]
			children.append(
				DeviceTreeNode(
					id=rtu.id,
					code=rtu.code,
					serial_no=rtu.serial_no or "",
					name=None,
					device_type=DeviceType.RTU.value,
					status=rtu.status,
					location_info=location_info,
					children=meter_children,
				)
			)

		return DeviceTreeNode(
			id=box.id,
			code=box.code,
			serial_no=box.serial_no or "",
			name=None,
			device_type=DeviceType.BOX.value,
			status=box.status,
			children=children,
		)

	# ============ 设备地图数据 ============
	@staticmethod
	async def get_device_map_data(db: DatabaseService) -> dict:
		try:
			# 台区表已移除gps字段，只返回基本信息
			stations = db.get_all(
				"""
				SELECT id, code, name, capacity_kva, province_code, city_code, area_code
				FROM dev_station
				WHERE deleted = 0
			"""
			)
		except Exception as e:
			logger.exception("查询台区信息失败")
			raise BizException.from_error_code(
				ErrorCode.DB_QUERY_ERROR, message="查询台区信息失败"
			) from e

		try:
			boxes = db.get_all(
				"""
				SELECT id, station_id, code, address, ST_X(gps) AS lng, ST_Y(gps) AS lat
				FROM dev_box
				WHERE deleted = 0 AND gps IS NOT NULL
			"""
			)
		except Exception as e:
			logger.exception("查询电箱GPS失败")
			raise BizException.from_error_code(
				ErrorCode.DB_QUERY_ERROR, message="查询电箱GPS失败"
			) from e

		try:
			rtus = db.get_all(
				"""
				SELECT r.id, r.box_id, r.serial_no, r.model, r.status, ST_X(b.gps) AS lng, ST_Y(b.gps) AS lat
				FROM dev_rtu r
				JOIN dev_box b ON r.box_id = b.id
				WHERE r.deleted = 0 AND b.deleted = 0 AND b.gps IS NOT NULL
			"""
			)
		except Exception as e:
			logger.exception("查询RTU GPS失败")
			raise BizException.from_error_code(
				ErrorCode.DB_QUERY_ERROR, message="查询RTU GPS失败"
			) from e

		return {
			"stations": [dict(s) for s in stations],
			"boxes": [dict(b) for b in boxes],
			"rtus": [dict(r) for r in rtus],
		}

	# ============ 替换/报废 ============
	@staticmethod
	async def replace_meter(
			db: DatabaseService, old_meter_id: int, payload: MeterCreate, operator_id: int
	) -> dict:
		"""
		电表替换:
		1. 创建新电表(继承box_id和rtu_id)
		2. 如果是三相表,迁移层级关系
		3. 将旧电表报废
		"""
		old = await get_meter_by_id(db, old_meter_id)
		if not old:
			raise BizException.from_error_code(
				ErrorCode.NOT_FOUND, message="旧电表不存在"
			)
		if old.status == DeviceStatus.DECOMMISSIONED:
			raise BizException.from_error_code(
				ErrorCode.BAD_REQUEST, message="已停用的电表不能替换"
			)

		async with db.transaction():
			# 继承box_id和rtu_id
			if not payload.box_id:
				payload.box_id = old.box_id
			if not payload.rtu_id:
				payload.rtu_id = old.rtu_id

			# 创建新电表
			new_id = await DeviceService.create_meter_with_validation(db, payload, operator_id)
			new_meter = await get_meter_by_id(db, new_id)

			# 如果是三相表,迁移层级关系
			migrated_count = 0
			if old.meter_type == 'THREE_PHASE':
				update_hierarchy_sql = """
					UPDATE dev_meter_hierarchy
					SET parent_meter_id = %s
					WHERE parent_meter_id = %s
				"""
				migrated_count = db.update(update_hierarchy_sql, (new_id, old_meter_id))
				logger.info(f"电表替换: 三相表({old_meter_id})迁移{migrated_count}个层级关系到新表({new_id})")

			# 将旧电表报废
			success = await update_meter(
				db,
				old_meter_id,
				MeterUpdate(status=DeviceStatus.DECOMMISSIONED),
			)
			if not success:
				raise BizException.from_error_code(
					ErrorCode.INTERNAL_SERVER_ERROR, message="旧电表停用失败"
				)

			# 逻辑删除旧电表
			delete_sql = "UPDATE dev_meter SET deleted = 1, updated_at = NOW() WHERE id = %s"
			db.update(delete_sql, (old_meter_id,))

			# 记录事件
			from app.crud.device_event_crud import create_device_event

			await create_device_event(
				db,
				{
					"target_type": "METER",
					"target_id": old_meter_id,
					"event_type": "REPLACED",
					"event_time": datetime.now(),
					"operator_id": operator_id,
					"related_device_id": new_id,
					"reason": "设备替换" + (f",迁移{migrated_count}个单相表" if migrated_count > 0 else ""),
					"old_value": old.serial_no or "",
					"new_value": new_meter.serial_no if new_meter else "",
				},
			)

		return {"old_meter_id": old_meter_id, "new_meter_id": new_id, "migrated_count": migrated_count}

	@staticmethod
	async def decommission_meter(
			db: DatabaseService,
			meter_id: int,
			operator_id: int,
			reason: Optional[str] = None,
	) -> dict:
		"""
		电表报废:
		1. 如果是三相表,删除层级关系
		2. 将电表状态设为已停用
		3. 逻辑删除
		"""
		meter = await get_meter_by_id(db, meter_id)
		if not meter:
			raise BizException.from_error_code(
				ErrorCode.NOT_FOUND, message="电表不存在"
			)

		async with db.transaction():
			# 删除层级关系
			deleted_count = 0
			if meter.meter_type == 'THREE_PHASE':
				delete_sql = "DELETE FROM dev_meter_hierarchy WHERE parent_meter_id = %s"
				deleted_count = db.update(delete_sql, (meter_id,))
				logger.info(f"电表报废: 三相表({meter_id})删除了{deleted_count}个层级关系")
			else:
				delete_sql = "DELETE FROM dev_meter_hierarchy WHERE child_meter_id = %s"
				deleted_count = db.update(delete_sql, (meter_id,))
				if deleted_count > 0:
					logger.info(f"电表报废: 单相表({meter_id})删除了层级关系")

			# 更新状态
			success = await update_meter(
				db,
				meter_id,
				MeterUpdate(status=DeviceStatus.DECOMMISSIONED),
			)
			if not success:
				raise BizException.from_error_code(
					ErrorCode.INTERNAL_SERVER_ERROR, message="电表报废失败"
				)

			# 逻辑删除
			delete_meter_sql = "UPDATE dev_meter SET deleted = 1, updated_at = NOW() WHERE id = %s"
			db.update(delete_meter_sql, (meter_id,))

			# 记录事件
			from app.crud.device_event_crud import create_device_event

			await create_device_event(
				db,
				{
					"target_type": "METER",
					"target_id": meter_id,
					"event_type": "DECOMMISSIONED",
					"event_time": datetime.now(),
					"operator_id": operator_id,
					"reason": reason or "正常报废",
				},
			)

		return {"deleted_hierarchy_count": deleted_count}

	# ----- 用户负责台区 -----
	@staticmethod
	async def get_managed_station_page(
			db: DatabaseService, query: DeviceQuery
	) -> PaginatedStation:
		total, rows = await get_station_list(db, query)
		items = [StationResponse.model_validate(s) for s in rows]
		pages = (total + query.size - 1) // query.size if query.size else 0
		return PaginatedStation(
			total=total,
			items=items,
			page=query.page,
			size=query.size,
			pages=pages,
			has_next=query.page * query.size < total,
			has_prev=query.page > 1,
		)

	@staticmethod
	async def get_stations_with_location_info(db: DatabaseService, stations: list) -> list:
		"""
		为台区列表添加位置信息
		"""
		from app.crud.device_crud import get_station_with_address
		from app.schemas.device_schemas import StationResponse

		station_responses = []
		for station in stations:
			try:
				# 获取台区的地址信息
				station_with_address = await get_station_with_address(db, station.id)
				if station_with_address:
					station_response = StationResponse.from_dict_with_address(station_with_address)
				else:
					# 如果获取地址信息失败，仍返回基本信息
					station_response = StationResponse.model_validate(station)
					station_response.location_info = []

				station_responses.append(station_response)
			except Exception as e:
				logger.warning(
					"获取台区地址信息失败，station_id=%s, error=%s", station.id, str(e)
				)
				# 即使地址信息获取失败，也要返回基本台区信息
				station_response = StationResponse.model_validate(station)
				station_response.location_info = []
				station_responses.append(station_response)

		logger.info("成功为%s个台区添加位置信息", len(station_responses))
		return station_responses

	# ----- 用户负责电箱 -----
	@staticmethod
	async def get_managed_box_page(
			db: DatabaseService, query: DeviceQuery
	) -> PaginatedBox:
		total, rows = await get_box_list(db, query)
		items = [BoxResponse.model_validate(b) for b in rows]
		pages = (total + query.size - 1) // query.size if query.size else 0
		return PaginatedBox(
			total=total,
			items=items,
			page=query.page,
			size=query.size,
			pages=pages,
			has_next=query.page * query.size < total,
			has_prev=query.page > 1,
		)

	# ----- 用户负责 RTU -----
	@staticmethod
	async def get_managed_rtu_page(
			db: DatabaseService, query: DeviceQuery
	) -> PaginatedRTU:
		total, rows = await get_rtu_list(db, query)
		items = [RTUResponse.model_validate(r) for r in rows]
		pages = (total + query.size - 1) // query.size if query.size else 0
		return PaginatedRTU(
			total=total,
			items=items,
			page=query.page,
			size=query.size,
			pages=pages,
			has_next=query.page * query.size < total,
			has_prev=query.page > 1,
		)

	@staticmethod
	async def get_boxes_with_location_info(db: DatabaseService, boxes: list) -> list:
		"""
		为电箱列表添加位置信息和station_name
		"""
		from app.crud.device_crud import get_box_with_address, get_station_by_id
		from app.schemas.device_schemas import BoxResponse

		box_responses = []
		for box in boxes:
			try:
				# 获取电箱的地址信息
				box_with_address = await get_box_with_address(db, box.id)
				if box_with_address:
					if hasattr(box, 'station_id') and box.station_id:

						try:
							station = await get_station_by_id(db, box.station_id)
							if station:
								box_with_address["station_name"] = station.name
						except Exception as e:
							logger.warning("获取电箱台区信息失败，box_id=%s, error=%s", box.id, str(e))

					box_response = BoxResponse.from_dict_with_address(box_with_address)
				else:
					# 如果获取地址信息失败，仍返回基本信息
					box_response = BoxResponse.model_validate(box)
					box_response.location_info = []
					if hasattr(box, 'station_id') and box.station_id:
						try:
							station = await get_station_by_id(db, box.station_id)
							if station:
								box_response.station_name = station.name
						except BizException as e:
							pass
				box_responses.append(box_response)
			except Exception as e:
				logger.warning(
					"获取电箱地址信息失败，box_id=%s, error=%s", box.id, str(e)
				)
				# 即使地址信息获取失败，也要返回基本电箱信息
				box_response = BoxResponse.model_validate(box)
				box_response.location_info = []
				if hasattr(box, 'station_id') and box.station_id:
					try:
						station = await get_station_by_id(db, box.station_id)
						if station:
							box_response.station_name = station.name
					except:
						pass
				box_responses.append(box_response)

		return box_responses

	@staticmethod
	async def get_box_with_complete_info(db: DatabaseService, box_id: int) -> dict:
		"""
		获取电箱的完整信息，包括station_name等关联信息
		并获取关联的文件列表
		"""
		from app.crud.device_crud import get_box_with_address, get_station_by_id

		# 获取电箱及其地址信息
		box_with_address = await get_box_with_address(db, box_id)
		if not box_with_address:
			return {}

		try:
			# 获取台区信息
			station = await get_station_by_id(db, box_with_address.get("station_id"))
			if station:
				box_with_address["station_name"] = station.name

			# 获取电箱关联的文件列表
			files = []
			try:
				# 使用file_crud中的方法获取业务关联文件
				file_infos = await get_files_by_business(db, 'BOX', str(box_id))
				# 转换FileInfo对象为字典列表，确保包含FileInfoResponse所需的所有字段
				files = []
				for file_info in file_infos:
					file_dict = {k: v for k, v in file_info.__dict__.items()}
					# 确保包含FileInfoResponse所需的所有必需字段
					file_dict.setdefault('status', 1)  # 默认设置为有效状态
					file_dict.setdefault('update_time', file_info.create_time)  # 使用创建时间作为更新时间
					file_dict.setdefault('biz_type', 'BOX')  # 设置业务类型
					file_dict.setdefault('biz_id', str(box_id))  # 设置业务ID
					file_dict.setdefault('md5', None)  # 设置MD5
					files.append(file_dict)
			except Exception as e:
				logger.warning("获取电箱文件列表失败，box_id=%s, error=%s", box_id, str(e))

			# 为每个文件生成带签名的URL
			for file in files:
				try:
					file['url'] = await file_service.generate_presigned_url(db, file['id'])
				except Exception as e:
					logger.warning("生成文件签名URL失败，file_id=%s, error=%s", file['id'], str(e))

			# 将文件列表添加到电箱信息中
			box_with_address["files"] = files

			logger.info("成功获取电箱完整信息，box_id=%s", box_id)
			return box_with_address
		except Exception as e:
			logger.warning(
				"获取电箱关联设备信息失败，box_id=%s, error=%s", box_id, str(e)
			)

			return box_with_address

	@staticmethod
	async def get_rtu_with_complete_info(db: DatabaseService, rtu_id: int) -> dict:
		"""
		获取RTU的完整信息，包括box_no、station_name等关联信息
		并获取关联的文件列表
		"""
		from app.crud.device_crud import get_rtu_with_address, get_box_by_id, get_station_by_id

		# 获取RTU及其地址信息
		rtu_with_address = await get_rtu_with_address(db, rtu_id)
		if not rtu_with_address:
			return {}

		try:
			# 获取电箱信息
			box = await get_box_by_id(db, rtu_with_address.get("box_id"))
			if box:
				rtu_with_address["box_no"] = box.code

				# 获取台区信息
				station = await get_station_by_id(db, box.station_id)
				if station:
					rtu_with_address["station_name"] = station.name

			# 获取RTU关联的文件列表
			files = []
			try:
				# 使用file_crud中的方法获取业务关联文件
				file_infos = await get_files_by_business(db, 'RTU', str(rtu_id))
				# 转换FileInfo对象为字典列表，确保包含FileInfoResponse所需的所有字段
				files = []
				for file_info in file_infos:
					file_dict = {k: v for k, v in file_info.__dict__.items()}
					# 确保包含FileInfoResponse所需的所有必需字段
					file_dict.setdefault('status', 1)  # 默认设置为有效状态
					file_dict.setdefault('update_time', file_info.create_time)  # 使用创建时间作为更新时间
					file_dict.setdefault('biz_type', 'RTU')  # 设置业务类型
					file_dict.setdefault('biz_id', str(rtu_id))  # 设置业务ID
					file_dict.setdefault('md5', None)  # 设置MD5
					files.append(file_dict)
			except Exception as e:
				logger.warning("获取RTU文件列表失败，rtu_id=%s, error=%s", rtu_id, str(e))

			# 为每个文件生成带签名的URL
			for file in files:
				try:
					file['url'] = await file_service.generate_presigned_url(db, file['id'])
				except Exception as e:
					logger.warning("生成文件签名URL失败，file_id=%s, error=%s", file['id'], str(e))

			# 将文件列表添加到RTU信息中
			rtu_with_address["files"] = files

			logger.info("成功获取RTU完整信息，rtu_id=%s", rtu_id)
			return rtu_with_address
		except Exception as e:
			logger.warning(
				"获取RTU关联设备信息失败，rtu_id=%s, error=%s", rtu_id, str(e)
			)

			return rtu_with_address

	@staticmethod
	async def get_meter_with_complete_info(db: DatabaseService, meter_id: int) -> dict:
		"""
		获取电表的完整信息，包括rtu_no、box_no、station_name
		复用已有方法，确保数据完整性
		"""

		# 获取电表及其地址信息
		meter_with_address = await get_meter_with_address(db, meter_id)
		if not meter_with_address:
			return {}

		try:
			# 获取RTU信息
			rtu = await get_rtu_by_id(db, meter_with_address.get("rtu_id"))
			if rtu:
				meter_with_address["rtu_no"] = rtu.code
				meter_with_address["rtu_serial_no"] = rtu.serial_no

				# 获取电箱信息
				box = await get_box_by_id(db, rtu.box_id)
				if box:
					meter_with_address["box_no"] = box.code
					meter_with_address["box_serial_no"] = box.serial_no

					# 获取台区信息
					station = await get_station_by_id(db, box.station_id)
					if station:
						meter_with_address["station_name"] = station.name

			# 获取电表关联的文件列表
			files = []
			try:
				file_infos = await get_files_by_business(db, 'METER', str(meter_id))
				files = []
				for file_info in file_infos:
					file_dict = {k: v for k, v in file_info.__dict__.items()}
					file_dict.setdefault('status', 1)  # 默认设置为有效状态
					file_dict.setdefault('update_time', file_info.create_time)  # 使用创建时间作为更新时间
					file_dict.setdefault('biz_type', 'METER')  # 设置业务类型
					file_dict.setdefault('biz_id', str(meter_id))  # 设置业务ID
					file_dict.setdefault('md5', None)  # 设置MD5
					files.append(file_dict)
			except Exception as e:
				logger.warning("获取电表文件列表失败，meter_id=%s, error=%s", meter_id, str(e))

			# 为每个文件生成带签名的URL
			for file in files:
				try:
					file['url'] = await file_service.generate_presigned_url(db, file['id'])
				except Exception as e:
					logger.warning("生成文件签名URL失败，file_id=%s, error=%s", file['id'], str(e))

			# 将文件列表添加到电表信息中
			meter_with_address["files"] = files

			return meter_with_address
		except Exception as e:
			logger.warning(
				"获取电表关联设备信息失败，meter_id=%s, error=%s", meter_id, str(e)
			)

			return meter_with_address

	@staticmethod
	async def get_rtus_with_location_info(db: DatabaseService, rtus: list) -> list:
		"""
		为RTU列表添加位置信息和station_name
		"""
		from app.crud.device_crud import get_rtu_with_address
		from app.schemas.device_schemas import RTUResponse

		rtu_responses = []
		for rtu in rtus:
			try:
				# 获取RTU的地址信息
				rtu_with_address = await get_rtu_with_address(db, rtu.id)
				if rtu_with_address:
					rtu_response = RTUResponse.from_dict_with_address(rtu_with_address)
				else:
					# 如果获取地址信息失败，仍返回基本信息
					rtu_response = RTUResponse.model_validate(rtu)
					rtu_response.location_info = []

				rtu_responses.append(rtu_response)
			except Exception as e:
				logger.warning(
					"获取RTU地址信息失败，rtu_id=%s, error=%s", rtu.id, str(e)
				)
				# 即使地址信息获取失败，也要返回基本RTU信息
				rtu_response = RTUResponse.model_validate(rtu)
				rtu_response.location_info = []
				rtu_responses.append(rtu_response)

		return rtu_responses

	@staticmethod
	async def get_meters_with_complete_info(db: DatabaseService, meters: list) -> list:

		meter_responses = []
		for meter in meters:
			try:
				# 获取电表的完整信息
				meter_complete_info = await DeviceService.get_meter_with_complete_info(
					db, meter.id
				)
				if meter.rtu_serial_no:
					meter_complete_info['rtu_serial_no'] = meter.rtu_serial_no
				if meter.box_serial_no:
					meter_complete_info['box_serial_no'] = meter.box_serial_no

				if meter_complete_info:
					meter_response = MeterResponse.from_dict_with_address(
						meter_complete_info
					)
				else:
					# 如果获取完整信息失败，仍返回基本信息
					meter_response = MeterResponse.model_validate(meter)
					meter_response.location_info = []
				meter_responses.append(meter_response)
			except Exception as e:
				logger.warning(
					"获取电表完整信息失败，meter_id=%s, error=%s", meter.id, str(e)
				)
				# 即使完整信息获取失败，也要返回基本电表信息
				meter_response = MeterResponse.model_validate(meter)
				meter_response.location_info = []
				meter_responses.append(meter_response)

		logger.info("成功为%s个电表添加完整信息", len(meter_responses))
		return meter_responses

	# ============ RTU替换与报废 ============
	@staticmethod
	async def replace_rtu(
			db: DatabaseService,
			old_rtu_id: int,
			new_rtu_payload: RTUCreate,
			operator_id: int
	) -> dict:
		"""
		RTU替换业务逻辑
		
		流程:
		1. 创建新RTU
		2. 将旧RTU下的所有电表迁移到新RTU
		3. 为每个被迁移的电表记录REPLACED事件
		4. 将旧RTU状态变更为已停用
		5. 记录设备替换事件
		
		Returns:
			dict: {'new_rtu_id': int, 'affected_meter_count': int}
		"""
		from app.crud.device_crud import update_rtu, delete_rtu
		from app.crud.device_event_crud import create_device_event
		
		# 检查旧RTU是否存在
		old_rtu = await get_rtu_by_id(db, old_rtu_id)
		if not old_rtu:
			raise BizException.from_error_code(ErrorCode.NOT_FOUND, message="旧RTU不存在")
		
		# 检查旧RTU状态是否允许替换
		if old_rtu.status == DeviceStatus.DECOMMISSIONED:
			raise BizException.from_error_code(ErrorCode.BAD_REQUEST, message="已停用的RTU不能替换")
		
		# 使用事务确保整个替换流程的原子性
		async with db.transaction():
			# 继承旧RTU的collector_type到新RTU
			if not new_rtu_payload.collector_type and old_rtu.collector_type:
				new_rtu_payload.collector_type = old_rtu.collector_type
				logger.info(f"RTU替换: 继承旧RTU的collector_type={old_rtu.collector_type}")
			
			# 创建新RTU
			new_rtu_id = await DeviceService.create_rtu_with_validation(db, new_rtu_payload, operator_id=operator_id)
			new_rtu = await get_rtu_by_id(db, new_rtu_id)
			
			# 调用CRUD层迁移电表
			from app.crud.device_crud import migrate_meters_to_new_rtu
			affected_count = await migrate_meters_to_new_rtu(db, old_rtu_id, new_rtu_id, new_rtu.serial_no)
			
			# 为每个被迁移的电表记录RTU替换事件
			if affected_count > 0:
				from app.crud.device_crud import get_meters_by_rtu_id
				meters = await get_meters_by_rtu_id(db, new_rtu_id)
				for meter in meters:
					meter_event = {
						"target_type": "METER",  # 事件主体是电表
						"target_id": meter["id"],  # 电表ID
						"target_serial_no": meter["serial_no"],  # 电表序列号
						"event_type": "REPLACED",
						"event_time": datetime.now(),
						"operator_id": operator_id,
						"related_device_id": new_rtu_id,  # 关联的新RTU
						"old_value": old_rtu.serial_no,  # 旧RTU序列号
						"new_value": new_rtu.serial_no,  # 新RTU序列号
						"reason": f"电表{meter['serial_no']}的RTU替换",
						"details": f'{{"meter_id":{meter["id"]},"meter_serial_no":"{meter["serial_no"]}","old_rtu_id":{old_rtu_id},"new_rtu_id":{new_rtu_id}}}'  
					}
					await create_device_event(db, meter_event)
			
			# 将旧RTU状态变更为已停用
			update_data = RTUUpdate(status=DeviceStatus.DECOMMISSIONED)
			success = await update_rtu(db, old_rtu_id, update_data)
			if not success:
				raise BizException.from_error_code(ErrorCode.INTERNAL_SERVER_ERROR, message="旧RTU停用失败")
			
			# 将旧RTU标记为逻辑删除
			await delete_rtu(db, old_rtu_id)
			logger.info(f"RTU替换: 旧RTU({old_rtu_id})已标记为逻辑删除")
			
			# 记录设备替换事件
			event_data = {
				"target_type": "RTU",
				"target_id": old_rtu_id,
				"target_serial_no": old_rtu.serial_no,
				"event_type": "REPLACED",
				"event_time": datetime.now(),
				"operator_id": operator_id,
				"related_device_id": new_rtu_id,
				"reason": f"设备替换,迁移{affected_count}个电表",
				"old_value": old_rtu.serial_no,
				"new_value": new_rtu.serial_no
			}
			await create_device_event(db, event_data)
			
		return {
			'new_rtu_id': new_rtu_id,
			'affected_meter_count': affected_count
		}
	
	@staticmethod
	async def decommission_rtu(
			db: DatabaseService,
			rtu_id: int,
			operator_id: int,
			reason: Optional[str] = None
	) -> int:
		"""
		RTU报废业务逻辑
		
		流程:
		1. 解绑关联的电表(rtu_id置空NULL)
		2. 为每个被解绑的电表记录UNBOUND事件
		3. 将RTU状态变更为已停用
		4. 记录设备事件
		
		Returns:
			int: 解绑的电表数量
		"""
		from app.crud.device_crud import update_rtu, delete_rtu
		from app.crud.device_event_crud import create_device_event
		
		# 检查RTU是否存在
		rtu = await get_rtu_by_id(db, rtu_id)
		if not rtu:
			raise BizException.from_error_code(ErrorCode.NOT_FOUND, message="RTU不存在")
		
		async with db.transaction():
			# 获取当前绑定的电表列表(用于记录解绑事件)
			from app.crud.device_crud import get_meters_by_rtu_id, unbind_meters_from_rtu
			meters = await get_meters_by_rtu_id(db, rtu_id)
			
			# 解绑关联的电表
			unbind_count = await unbind_meters_from_rtu(db, rtu_id)
			logger.info(f"RTU报废: 解绑{unbind_count}个电表")
			
			# 为每个被解绑的电表记录UNBOUND事件
			for meter in meters:
				meter_event = {
					"target_type": "METER",  # 事件主体是电表
					"target_id": meter["id"],  # 电表ID
					"target_serial_no": meter["serial_no"],  # 电表序列号
					"event_type": "UNBOUND",
					"event_time": datetime.now(),
					"operator_id": operator_id,
					"related_device_id": rtu_id,  # 关联的RTU
					"old_value": rtu.serial_no,  # 旧RTU序列号
					"new_value": None,  # 解绑后无RTU
					"reason": f"RTU报废,解绑电表{meter['serial_no']}",
					"details": f'{{"meter_id":{meter["id"]},"meter_serial_no":"{meter["serial_no"]}","rtu_id":{rtu_id}}}'  
				}
				await create_device_event(db, meter_event)
			
			# 更新状态为已停用
			update_data = RTUUpdate(status=DeviceStatus.DECOMMISSIONED)
			success = await update_rtu(db, rtu_id, update_data)
			if not success:
				raise BizException.from_error_code(ErrorCode.INTERNAL_SERVER_ERROR, message="RTU报废失败")
			
			# 将RTU标记为逻辑删除
			await delete_rtu(db, rtu_id)
			logger.info(f"RTU报废: RTU({rtu_id})已标记为逻辑删除")
			
			# 记录设备事件
			event_data = {
				"target_type": "RTU",
				"target_id": rtu_id,
				"target_serial_no": rtu.serial_no,
				"event_type": "DECOMMISSIONED",
				"event_time": datetime.now(),
				"operator_id": operator_id,
				"reason": reason or "正常报废"
			}
			await create_device_event(db, event_data)
			
		return unbind_count
